package org.gbif.metrics.ws.resources;

import org.gbif.api.exception.ServiceUnavailableException;
import org.gbif.api.model.metrics.cube.Rollup;
import org.gbif.metrics.cube.CubeIo;
import org.gbif.metrics.cube.CubeIo.Type;
import org.gbif.metrics.cube.index.UuidIntMap;
import org.gbif.metrics.cube.index.taxon.OccurrenceDatasetCube;
import org.gbif.metrics.cube.occurrence.OccurrenceCube;
import org.gbif.metrics.ws.resources.provider.ProvidedOccurrenceCubeReader;
import org.gbif.ws.util.ExtraMediaTypes;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Ordering;
import com.google.inject.Inject;
import com.urbanairship.datacube.Address;
import com.urbanairship.datacube.DataCubeIo;
import com.urbanairship.datacube.ReadBuilder;
import com.urbanairship.datacube.ops.LongOp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A simple generic resource that will look up a numerical count from the named cube
 * and address provided. Should no address be provided, a default builder which counts
 * all records is used.
 */
@Path("/occurrence")
@Produces({MediaType.APPLICATION_JSON, ExtraMediaTypes.APPLICATION_JAVASCRIPT})
public class OccurrenceCubeResource {

  private static final Logger LOG = LoggerFactory.getLogger(OccurrenceCubeResource.class);

  // Since the cube does not support a count for all records, we do a basic sum
  // Total = georeferenced + nonGeoferenced records
  private static final Address GEOREFERENCED = new ReadBuilder(OccurrenceCube.INSTANCE)
                                                   .at(OccurrenceCube.IS_GEOREFERENCED, true).build();

  private static final Address NOT_GEOREFERENCED = new ReadBuilder(OccurrenceCube.INSTANCE)
                                                   .at(OccurrenceCube.IS_GEOREFERENCED, false).build();

  private final DataCubeIo<LongOp> occCubeIo;
  private final DataCubeIo<UuidIntMap> taxCubeIo;


  @Inject
  public OccurrenceCubeResource(@CubeIo(Type.OCCURRENCE) DataCubeIo<LongOp> occCubeIo,
                                @CubeIo(Type.TAXON_OCCURRENCE_DATASET) DataCubeIo<UuidIntMap> taxCubeIo) {
    this.occCubeIo = occCubeIo;
    this.taxCubeIo = taxCubeIo;
  }

  /**
   * Looks up an addressable count from the cube, using the given {@link ReadBuilder}.
   */
  @GET
  @Path("/count")
  public Long count(@ProvidedOccurrenceCubeReader ReadBuilder b) {
    Address a = b.build();
    try {
      // If no address, perform a SUM to get all
      if (a.getBuckets().isEmpty()) {
        return lookup(GEOREFERENCED) + lookup(NOT_GEOREFERENCED);
      } else {
        return lookup(a);
      }
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("The provided address is not calculated in the cube: " + a);
    } catch (Exception e) {
      LOG.error("Unable to read from the cube", e);
      throw new ServiceUnavailableException("Unable to read from the occurrence cube", e);
    }
  }

  /**
   * @return The public API schema
   */
  @GET
  @Path("/count/schema")
  public List<Rollup> getSchema() {
    // External Occurrence cube definition
    return org.gbif.api.model.metrics.cube.OccurrenceCube.ROLLUPS;
  }

  @GET
  @Path("/datasets")
  public SortedMap<UUID, Integer> get(@QueryParam("nubKey") int nubKey) {
    try {
      Optional<UuidIntMap> o = taxCubeIo.get(
        new ReadBuilder(OccurrenceDatasetCube.INSTANCE).at(OccurrenceDatasetCube.NUB_KEY, nubKey));
      if (o.isPresent()) {
        return sortDescending(o.get().getCounts());
      } else {
        return sortDescending(UuidIntMap.EMPTY_MAP.getCounts());
      }
    } catch (Exception e) {
      e.printStackTrace();
      LOG.error("Unable to read from the cube", e);
      throw new ServiceUnavailableException("Unable to read from the taxon occurrence dataset cube", e);
    }
  }

  /**
   * @return an immutable copy of the original, with the types casted and ordered
   *         by the value descending
   */
  @VisibleForTesting
  SortedMap<UUID, Integer> sortDescending(Map<CharSequence, Integer> source) {
    // Cast the input to the correct type
    ImmutableSortedMap.Builder<UUID, Integer> b = ImmutableSortedMap.<UUID, Integer>naturalOrder();
    for (Map.Entry<CharSequence, Integer> e : source.entrySet()) {
      b.put(UUID.fromString(String.valueOf(e.getKey())), e.getValue());
    }
    ImmutableSortedMap<UUID, Integer> typedMap = b.build();

    // Sort
    // Note: Compound is NEEDED to handle records with equal values
    // A unit test exists to covers this
    return ImmutableSortedMap.copyOf(typedMap,
      Ordering.natural().onResultOf(Functions.forMap(typedMap)).compound(Ordering.natural()).reverse());
  }

  /**
   * Simple lookup utility
   * 
   * @param a To lookup
   * @return The count, which may be calculated to be 0
   * @throws IOException On communication to the cube (typically an HBase layer)
   * @throws InterruptedException If something nasty happens within the cube (e.g.) some fatal service error
   * @throws IllegalArgumentException Should the provided Address NOT be calculated in the cube
   */
  private Long lookup(Address a) throws IOException, InterruptedException, IllegalArgumentException {
    Optional<LongOp> o = occCubeIo.get(a);
    if (o.isPresent()) {
      return o.get().getLong();
    } else {
      return 0L;
    }
  }
}
